%include "lib.inc"

global _start       ; 将符号_start导出给外部使用，在linker.ld中，已经将_start配置为grub引导（bootloader）加载内核后的入口地址

global printstr
global idt_init_asm
global paging_init_asm
global idt_timer_handler
global reload_segment_registers
global swtich_to_task
global idt_system_call_handler
global _ticks
global printint
global idt_keyboard_handler
global print_char
global print_str_endwith_enter

extern GDT_BASE     ; 外部提供的变量
extern GDT_LIMIT

extern get_address_of_process
extern OFFSET_OF_LDT_SELECTOR
extern schedule
extern SYSCALL_HANDLE_TABLE
extern save_all_regsiters
extern keyboard_handler

SECTION .text
bits 32             ; 注意，grub引导加载内核之后，处于32位保护模式之下，因此此时仍需要使用32位
_start:
    mov [_EAX_magic_number], eax
    mov [_EBX_multiboot_info], ebx
    mov eax, 0B8000h
    mov [_VGA_disp_cuurent_pos], eax        ; VGA显示区起始地址

    mov esp, 0x400000                        ; 临时指定一个堆栈

    ; 清屏幕
    call CleanScreen

;    call Next
;Next:
;    pop eax
;    call DispInt            ; 打印EIP
;    call DispReturn

    call _get_mem_info


    ; 进入内核
    extern kernel_start
    call kernel_start
    call DispReturn

    hlt             ; 终止程序


_EAX_magic_number       dd  0       ; boot loader完成引导后保存EAX
_EBX_multiboot_info     dd  0       ; boot loader完成引导后保存EBX，指向boot loader阶段通过BIOS获取的系统信息


;===========================================================
;       GDT/LDT初始化
;===========================================================

; 全局描述符表初始化
_GDT_LIMIT      equ 0400h 
_KERNEL_CS      equ 08h
_KERNEL_DS      equ 010h
_KERNEL_STACK   equ 018h
_KERNEL_TSS     equ 020h
;========================================
; GDT表初始化后，重新加载段寄存器和GDTR寄存器
; rust原型：fn reload_segment_registers()
;========================================
reload_segment_registers:
    
    ; 准备gdtr需要的数据，GDT表界限和GDT基址
    add esp, 6
    xor eax, eax
    mov ax, [GDT_LIMIT]
    dec ax
    mov word [esp], ax
    mov eax, [GDT_BASE]
    mov dword [esp + 2], eax
    lgdt [esp]  ; 加载到gdtr
    sub esp, 6

    ; 重新加载各个段寄存器
    jmp _KERNEL_CS:.load_cs     ; 通过jmp来加载CS:EIP
.load_cs:
    mov ax, _KERNEL_DS
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax

    mov ax, _KERNEL_STACK
    mov ss, ax

    ; 加载TSS
    mov ax, _KERNEL_TSS
    ltr ax

    ret

;===========================================================
;       中断向量表和中断函数初始化
;===========================================================

; 中断向量表初始化
_IDT_BASE   equ     02000h
_IDT_LIMIT  equ     0800h
_IDT_REG    dw      _IDT_LIMIT - 1
            dd      _IDT_BASE
idt_init_asm:
    mov ecx, 255
    xor eax, eax                                ; idt偏移
.set_idt_entry:
    mov edx, _idt_default_handler               ; 默认中断处理函数
    mov word [_IDT_BASE + eax], dx              ; 段偏移的低16位
    mov word [_IDT_BASE + eax + 2], _KERNEL_CS  ; 目标段选择符
    mov byte [_IDT_BASE + eax + 4], 0           ; 参数个数，低5位
    mov byte [_IDT_BASE + eax + 5], 08Eh        ; 段属性， 8eh - 32位中断门
    shr edx, 16
    mov word [_IDT_BASE + eax + 6], dx          ; 段偏移的高16位
    add eax, 8
    loop .set_idt_entry

    ; 加载IDTR
    lidt [_IDT_REG]
    
    ; 中断处理函数
    extern handler_init
    call handler_init

    ;sti                 ; 开中断，这样就可以接收到8259A连接的外设的中断
    cli

    ret

;===========================================================
;       分页初始化
;===========================================================

; 分页初始化
; 暂时在内核中映射32位的全部4G物理地址，需要4M+4K的空间
; 采用2级页表：页目录表 + 页表
; 均需要4K对齐
_PAGE_DIR_TABLE     equ     0100000h
_PAGE_TABLE         equ     0101000h
paging_init_asm:
    call _setup_page_dir        ; 初始化页表目录
    call _setup_page_table      ; 初始化页表
    
    ; 接下来要开启分页功能
    mov eax, _PAGE_DIR_TABLE            ; 页目录基地址（物理地址）
    mov cr3, eax                        ; 将页目录表基地址(物理地址)存起来
    mov eax, cr0
    or  eax, 80000000h                  ; 开启分页，PG=1（第31位）
    mov cr0, eax
    
    jmp .paging_ok
.paging_ok:

    ret

PA_P        equ 0001h       ; 页面存在
PA_RW       equ 0002h       ; 页面可读可写
PA_USER     equ 0004h       ; 页面为普通用户权限
PA_PS_4K    equ 0000h       ; 每个页大小为4k
; 页目录表初始化
_setup_page_dir:
    ; stosd --- eax -> [es:edi]
    push edi
    
    mov edi, _PAGE_DIR_TABLE
    mov eax, _PAGE_TABLE        ; 页表基地址
    add eax, PA_P + PA_RW + PA_USER         ; 先置为普通用户可写可读（后续进一步做内核和普通程序分区后再修改）
    mov ecx, 1024           ; 页目录共1024项
.setup_pagedir_entry:
    stosd                   ; 页目录表项1
    add eax, 01000h         ; 每一个页表的大小为4K
    loop .setup_pagedir_entry

    pop edi
    ret

PA_START        equ     00000000h       ; 起始物理地址
; 初始化页表
_setup_page_table:
    ; stosd --- eax -> [es:edi]
    push edi

    mov edi, _PAGE_TABLE            ; 页表起始地址（线性地址）
    mov eax, PA_START               ; 物理页从地址0开始
    add eax, PA_P + PA_RW + PA_USER
    ;mov ecx, 0100000h               ; 1个页表有1024项，共1024个页表 1024 * 1024 = 0100000h
    mov ecx, 1024 * 512             ;  目前最多只能初始化766，先初始化512个页表，TODO：获取系统的内存分布
.setup_one_page:
    stosd
    add eax, 01000h
    loop .setup_one_page

    pop edi
    ret

; 获取内存信息
_get_mem_info:
    mov eax, [_EBX_multiboot_info]
    mov ecx, [eax]
    add eax, 8
.read_tag:
    mov ebx, [eax]  ; tag

    cmp ebx, 0
    je .finished    ; 结束
    cmp ebx, 6      ; 是否是内存信息
    je .mem_info
    mov edx, [eax + 4]
    and edx, 07h        ; 8字节对齐
    cmp edx, 0
    jne .align_8
    mov edx, [eax + 4]
    jmp .algin_ok
.align_8:
    mov edx, [eax + 4]
    and edx, 0fffffff8h
    add edx, 8
.algin_ok:    
    add eax, edx
    jmp .read_tag
.mem_info:
    mov edx, eax

    push eax
    push ecx
    push ebx

    mov eax, [edx + 4]
    sub eax, 16

    xor ebx, ebx
    mov ebx, [edx + 8]  ; 当前固定是24字节
    div bl          ; eax=eax/bl
    mov ecx, eax
    mov eax, edx
    add eax, 16

    call _get_one_mem_entry

    pop ebx
    pop ecx
    pop eax

    mov edx, [eax + 4]
    and edx, 07h        ; 8字节对齐
    cmp edx, 0
    jne .align_8
    mov edx, [eax + 4]
    jmp .algin_ok
.align_8_1:
    mov edx, [eax + 4]
    and edx, 0fffffff8h
    add edx, 8
.algin_ok_1:

    add eax, edx
    jmp .read_tag

.finished:
    ret

_mem_disp_str db "Base:Low High        Size:Low High        Type"
_mem_disp_str_len equ $ - _mem_disp_str
;==============================
; ecx --- times
; eax --- start address
_get_one_mem_entry:
    pusha
    push _mem_disp_str_len
    push _mem_disp_str
    call printstr
    call DispReturn
    add esp, 8
    popa

    push edx
    mov edx, eax
.loop_mem:
    mov eax, [edx + 0]
    call DispInt
    call DispSpace
    mov eax, [edx + 4]
    call DispInt
    call DispSpace
    call DispSpace
    call DispSpace
    call DispSpace
    mov eax, [edx + 8]
    call DispInt
    call DispSpace
    mov eax, [edx + 12]
    call DispInt
    call DispSpace
    call DispSpace
    call DispSpace
    call DispSpace
    mov eax, [edx + 16]
    call DispInt
    call DispReturn

    add edx, 24
    loop .loop_mem

    pop edx
    ret

;===========================================================
;       中断处理函数
;===========================================================
_int_times dd '0'
;==========================================
; 默认中断处理函数
_idt_default_handler:
    ;mov ah, 0ch     ; 0000 黑底， 000C 红字
    mov eax, [_int_times]
    ;mov edi, 0B8000h + (80 * 15 + 0) * 2
    ;mov al, 'I'
    ;mov [gs:edi], eax
    ;inc byte [gs:edi]
    inc eax
    mov [_int_times], eax
    ;call _send_eoi
    iretd

_send_eoi:
    mov al, 020h
    out 20h, al         ; 向主8259A发送EOI
    ret

;===================================
; 系统中断处理函数，其所在的中断描述符的DPL=3
;   中断向量号：0x80
;   eax --- 存放系统调用索引
_result dd 0
_cur_p_nr dd 0
idt_system_call_handler:
    mov [_result], eax
    extern current_process_nr
    mov eax, [current_process_nr]
    mov [_cur_p_nr], eax


    call _save

    sti
    ; 根据系统调用索引调用具体的接口
    mov eax, [_result]
    call [SYSCALL_HANDLE_TABLE + eax * 4]
    extern save_return_value_to_eax
    push dword [_cur_p_nr]
    push eax
    call save_return_value_to_eax
    add esp, 8
    cli

    ret

; 键盘中断处理函数
idt_keyboard_handler:
    call _save

    ; send EOI
    mov al, 020h
    out 20h, al

    ;sti    ; 这里开中断的话，居然会碰到扩展键盘的扫描码时候，扩展字E0会出现在实际扫描码后面，例如E0 48实际收到的顺序是48 E0
    call keyboard_handler
    ;cli

    ret

;===================================
; 时钟中断处理函数
;   中断向量号：0x20
_ticks dd 0
idt_timer_handler:
    call _save
    inc dword [_ticks]
    
    sti
    ; 调度一个进程
    xor eax, eax
    call schedule
    cli

    ; send EOI
    mov al, 020h
    out 20h, al

    ret

; 保存相关的寄存器
; esp: 函数返回地址
; esp + 4*n : 各个寄存器
; 如果进入中断前已经是内核态，说明是中断重入
; 那么按照普通的调用返回即可
; 否则需要将用户态的寄存器保存，便于后续进行恢复
; 1) 用户态->内核态：需要将寄存器保存到当前任务中
; 2) 内核态->内核态：说明是中断重入，不需要保存寄存器到当前任务，因为是从内核栈进入，不会切换栈
_reenter dd 0
RET_ADDRESS_OF_SAVE_OFFSET_IN_STACKTOP equ 52   ; _save返回地址对应栈顶的偏移（52字节包含 _restart/_restart_reenter地址4字节、gs等寄存器16字节、eax等通用寄存器32字节）
_save:
    ;sub esp, 4 ； 已经被_save的返回地址填充了
    pushad
    push ds
    push es
    push fs
    push gs

    cmp dword [_reenter], 0
    jne .2
    ; 如果_reenter等于0，表明是从内核态->内核态，则不需要保存寄存器
.save_regs_to_process: ; 如果_reenter等于0，表明是从用户态->内核态，需要将寄存器保存到任务中
    inc dword [_reenter]
    ; 将寄存器保存到当前进程
    call save_all_regsiters

    ; 如果非中断重入，那么中断返回ret时执行的地址为_restart
    push _restart
    ; 跳到_save的返回地址处开始执行，注意，在_save已经保存好中断处理的ret返回地址
    jmp [esp + RET_ADDRESS_OF_SAVE_OFFSET_IN_STACKTOP]

.2: 
    inc dword [_reenter]
    ; 如果中断重入，那么中断返回ret时执行的地址为_restart_reenter
    push _restart_reenter
    ; 跳到_save的返回地址处开始执行，注意，在_save已经保存好中断处理的ret返回地址
    jmp [esp + RET_ADDRESS_OF_SAVE_OFFSET_IN_STACKTOP]

_restart:           ; 中断正常从内核态返回到用户态处理
    ; 需要将待切换的进程的寄存器恢复
    ; 待调度的进程
    xor eax, eax
    call get_address_of_process
    ; 加载LDT
    xor edx, edx
    mov edx, [OFFSET_OF_LDT_SELECTOR]
    add edx, eax
    mov cx, [edx]
    lldt cx  ; 加载到ldtr
    ; 切换到目标进程的堆栈
    mov esp, eax

_restart_reenter:   ; 中断重入返回处理，仍然是从内核态到内核态    
    pop gs
    pop fs
    pop es
    pop ds
    popad
    add esp, 4

    dec dword [_reenter]
    iretd

_task_init_flag db 1
swtich_to_task:
    mov byte [_task_init_flag], 0       ; 保证先切换到task0，然后再启动进程调度
    ;call swtich_to_task_ret_far
    call swtich_to_task_iretd
    ret

swtich_to_task_ret_far:
    ; 加载LDT
    mov ax, 72
    lldt ax  ; 加载到ldtr,是16位的选择符

    ; 要将task0的相关寄存器恢复
    extern task0_run
    push 01Fh                       ; 特权级3的堆栈ss
    push 0x405000                   ; 特权级3的esp
    push 0Fh                        ; 特权级3的代码段选择符
    push task0_run                  ; 特权级3代码段的偏移地址，实际就是入口地址

    retf

swtich_to_task_iretd:
    extern get_address_of_process
    extern OFFSET_OF_LDT_SELECTOR
    extern OFFSET_OF_STACK_FOR_IRET

    push 0
    xor eax, eax
    call get_address_of_process
    add esp, 4

    ; 加载LDT
    xor edx, edx
    mov edx, [OFFSET_OF_LDT_SELECTOR]
    add edx, eax
    mov cx, [edx]
    lldt cx  ; 加载到ldtr

    mov esp, eax
    pop gs
    pop fs
    pop es
    pop ds
    popad
    add esp, 4

    ; 从iretd返回时候的EFLAGS已经打开了IF标志位，因此不需要sti指令开启中断
    
    iretd                          ; 模拟中断返回，会从特权级0切换到特权级3
